package tcln.sors.client.core.component.grid;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

import org.apache.commons.beanutils.BeanUtils;

import tcln.commons.assertion.Assert;
import tcln.commons.exception.BaseRuntimeException;
import tcln.commons.lang.comparator.ComparatorCreator;
import tcln.sors.client.core.mvc.ModelData;

/**
 * A simple grid containing table-like data.
 * <p>
 * This grid, basically, is like a {@link JTable}. This means, it shares the same concepts of a table such as column
 * model, data model, cell renderer, cell editor... However, unlike a table, it supports the following features:
 * <ul>
 * <li>It supports building a grid from {@link ModelData}s. Each {@link ModelData} represents a row in the grid.
 * <li>It does not require to be put in a {@link JScrollPane} since this grid, in fact, a scroll-pane.
 * <li>It supports adding columns after this grid is rendered to its container.
 * <li>Via extensible way of implementation, subclasses are free to customize several parts.
 * </ul>
 * There are some rules while using this grid:
 * <ul>
 * <li>By construction a grid, this grid is considered 'pre-rendered'.
 * <li>This grid's state is changed to rendered only after method {@link #renderTo(JComponent, Object)} is invoked.
 * <b>This step is crucial for this grid to render correctly</b>. Missing invoking this method would probably lead to a
 * runtime exception.
 * </ul>
 * 
 * @param <M>
 *            Model data type.
 * @author Truong Chau Lien Ngoc (lienngoc@gmail.com)
 */
@SuppressWarnings("serial")
public class JGrid<M extends ModelData<?>> extends JScrollPane {

    private boolean rendered;
    private int columnCount = 0;
    private JTable innerTable;
    private Comparator<M> modelComparator;

    /**
     * Constructs this grid with a column model.
     */
    public JGrid() {
        super(VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED);

        // by default, equals() is used for comparison.
        modelComparator = ComparatorCreator.getInstance(new Comparator<M>() {
            @Override
            public int compare(M o1, M o2) {
                return o1.equals(o2) ? 0 : 1;
            }
        });

        TableColumnModel columnModel = createColumnModel();
        TableModel tableModel = createTableModel();

        innerTable = createTable();
        innerTable.setModel(tableModel);
        innerTable.setColumnModel(columnModel);

        setViewportView(innerTable);
    }

    /**
     * Sets a model comparator to compare models.
     * 
     * @param c
     * @return
     */
    public JGrid<M> setModelComparator(Comparator<M> c) {
        Assert.ARGUMENTS.notNull(c, "Custom comparator must not be null");
        this.modelComparator = c;
        return this;
    }

    /**
     * Returns if this grid has been rendered or not.
     * 
     * @return the {@link #rendered}.
     */
    public boolean isRendered() {
        return rendered;
    }

    /**
     * (pre-rendered) Adds a new column to this grid.
     * 
     * @param column
     *            Column information. Notice that column identifier will be used as the property name and used to access
     *            corresponding model data.
     * @return Current grid instance.
     */
    public final JGrid<M> addColumn(TableColumn column) {
        column.setModelIndex(columnCount++);

        // always use String presentation
        if (!(column.getIdentifier() instanceof String)) {
            column.setIdentifier(String.valueOf(column.getIdentifier()));
        }

        // because of the risk of adding a column twice in the table, just add this column identifier to current table
        // model. The whole column will be added into current column model.
        getEditableTableModel().addColumnInternal(column.getIdentifier());
        innerTable.getColumnModel().addColumn(column);

        return this;
    }

    /**
     * (pre-rendered) Adds a new column to this grid.
     * 
     * @param id
     *            Unique ID of this column in the column model.
     * @param title
     *            The heading value displayed as the title of this column.
     * @param width
     *            Column width.
     * @param resizable
     *            <code>true</code> to let this column be resizable.
     * @return Current grid instance.
     */
    public final JGrid<M> addColumn(String id, String title, int width, boolean resizable) {
        TableColumn column = createNewColumn();
        column.setIdentifier(id);
        column.setPreferredWidth(width);
        column.setResizable(resizable);
        column.setHeaderValue(title);
        return addColumn(column);
    }

    /**
     * Clears old models and adds new models.
     * 
     * @param models
     */
    public final void setModels(List<M> models) {
        ensureTableModelConstraints();
        removeAllModels();
        addModels(models);
    }

    /**
     * Adds given models into this grid.
     * 
     * @param models
     */
    public final void addModels(M... models) {
        Assert.ARGUMENTS.notNull(models, "Adding models must be provided");
        Assert.ARGUMENTS.isFalse(models.length == 0, "Adding models must be provided");
        addModels(Arrays.asList(models));
    }

    /**
     * Adds given models into this grid.
     * 
     * @param models
     */
    public final void addModels(List<M> models) {
        ensureTableModelConstraints();
        Assert.ARGUMENTS.notEmpty(models, "Adding models must be provided");

        JGridTableModel tableModel = getEditableTableModel();
        TableColumnModel columnModel = getColumnModel();

        for (M model : models) {
            GridRowData<M> rowData = new GridRowData<M>(model, columnCount);

            for (int i = 0; i < columnCount; i++) {
                TableColumn column = columnModel.getColumn(i);
                String property = (String) column.getIdentifier();
                Object propertyValue = model.get(property, Object.class);
                rowData.add(propertyValue);
            }

            tableModel.addRow(rowData);
        }
    }

    /**
     * Removes a given model.
     * 
     * @param model
     * @return <code>true</code> if given model has been removed.
     */
    public final boolean removeModel(M model) {
        ensureTableModelConstraints();
        int removingIndex = indexOfModel(model);
        if (removingIndex >= 0) {
            return removeModel(removingIndex);
        }
        return false;
    }

    /**
     * Removes a model at given index.
     * 
     * @param rowIndex
     * @return
     */
    public final boolean removeModel(int rowIndex) {
        ensureTableModelConstraints();
        JGridTableModel tableModel = getEditableTableModel();
        List<GridRowData<M>> rows = getRows();

        if (rowIndex >= 0 && rowIndex < rows.size()) {
            rows.get(rowIndex).clearModel();
            tableModel.removeRow(rowIndex);
            return true;
        }
        return false;
    }

    /**
     * Returns all models.
     * 
     * @return A new list containing all models.
     */
    public final List<M> getAllModels() {
        ensureTableModelConstraints();
        List<GridRowData<M>> rows = getRows();

        List<M> allModels = new ArrayList<M>(rows.size());
        for (GridRowData<M> gridRowData : rows) {
            allModels.add(gridRowData.getModel());
        }

        return allModels;
    }

    /**
     * Returns the index of given model in current table.
     * 
     * @param model
     *            Model to be searched.
     * @return Model index (or row index) if possible or <b>-1</b> otherwise.
     */
    public final int indexOfModel(M model) {
        ensureTableModelConstraints();
        List<GridRowData<M>> rows = getRows();

        // find out index of the model
        for (int i = 0; i < rows.size(); i++) {
            GridRowData<M> rowData = rows.get(i);
            if (modelComparator.compare(rowData.getModel(), model) == 0) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Returns the corresponding model object at given row index.
     * 
     * @param rowIndex
     * @return
     */
    public final M getModelAt(int rowIndex) {
        if (rowIndex >= 0 && rowIndex < getTableModel().getRowCount()) {
            ensureTableModelConstraints();
            return getRows().get(rowIndex).getModel();
        }
        return null;
    }

    /**
     * Removes all models, one by one. Notice that depending on the actual implementation of {@link TableModel}, events
     * may fire one or many times.
     * 
     * @return Current grid instance.
     */
    public final JGrid<M> removeAllModels() {
        ensureTableModelConstraints();
        int rowCount = getTableModel().getRowCount();
        for (int i = 0; i < rowCount; i++) {
            removeModel(0);
        }
        return this;
    }

    /**
     * Adds this grid into given owner component using given layout constraints.
     * <p>
     * By design, this method is necessary to make this grid behave correctly (e.g. set flag 'rendered' to
     * <code>true</code>). Thus, this method must be called before models can be added into this grid.
     * 
     * @param ownerComponent
     *            Owner component.
     * @param constraints
     *            All applicable constraints.
     */
    public void renderTo(JComponent ownerComponent, Object constraints) {
        Assert.ARGUMENTS.notNull(ownerComponent, "Owner component is required");
        if (constraints != null) {
            ownerComponent.add(this, constraints);
        } else {
            ownerComponent.add(this);
        }

        rendered = true;

        onRender();
    }

    /**
     * Fires after this grid is added to its owner component.
     */
    protected void onRender() {
        // template
    }

    /**
     * Creates a new table column instance on adding a new column.
     * 
     * @return New table column to be added.
     */
    protected TableColumn createNewColumn() {
        return new TableColumn();
    }

    /**
     * Creates a new table column instance on adding a new column.
     * 
     * @param source
     *            Source table column.
     * @return New table column to be added.
     */
    protected final TableColumn createNewColumn(TableColumn source) {
        TableColumn column = createNewColumn();
        try {
            BeanUtils.copyProperties(column, source);
        } catch (Exception e) {
            BaseRuntimeException.wrapIfNecessary(e);
        }
        return column;
    }

    /**
     * Creates a new column model for this grid. Subclasses may override this method to provide another implementation.
     * 
     * @return
     */
    protected TableColumnModel createColumnModel() {
        return new DefaultTableColumnModel();
    }

    /**
     * Creates a new table for this grid. Subclasses may override this method to provide another implementation.
     * 
     * @return
     */
    protected JTable createTable() {
        JTable table = new JTable();
        initializeTable(table);
        return table;
    }

    /**
     * Initializes given table just after a new {@link JTable} instance has been created inside method
     * {@link #createTable()}.
     * 
     * @param table
     */
    protected void initializeTable(JTable table) {
        // template
    }

    /**
     * Creates a new table model for this grid.
     * 
     * @return a new {@link JGridTableModel} instance.
     */
    protected TableModel createTableModel() {
        return new JGridTableModel();
    }

    /**
     * Checks to ensure that this grid supports adding, removing and getting (by index) model data.
     */
    protected void ensureTableModelConstraints() {
        Assert.STATE.isTrue(getTableModel() instanceof JGridTableModel, "Table model must be a {0}",
                JGridTableModel.class);
    }

    /**
     * Returns current table's column model.
     * 
     * @return
     */
    protected TableColumnModel getColumnModel() {
        return innerTable.getColumnModel();
    }

    /**
     * Returns current table's model.
     * 
     * @return
     */
    protected final TableModel getTableModel() {
        return innerTable.getModel();
    }

    /**
     * Returns current table's model for editing. The expectation is the the implementation of table model must be a
     * {@link JGridTableModel}.
     * 
     * @return
     */
    protected final JGridTableModel getEditableTableModel() {
        return (JGridTableModel) innerTable.getModel();
    }

    /**
     * Returns all rows - ie. {@link GridRowData}s - of this table.
     * 
     * @return
     */
    @SuppressWarnings("unchecked")
    protected List<GridRowData<M>> getRows() {
        return getEditableTableModel().getDataVector();
    }
}
